package org.filecollector.filecontroller.virtualcatalogue;

import org.filecollector.filecontroller.virtualcatalogue.virtualfile.FileTroubles;
import org.filecollector.filecontroller.virtualcatalogue.virtualfile.InvalidFileException;
import org.filecollector.filecontroller.virtualcatalogue.virtualfile.VirtualFile;

import java.io.File;
import java.nio.file.Path;
import java.util.*;
import java.util.function.*;
import java.util.stream.Stream;

/**
 * Class that contains business logic of this package
 */
abstract class VirtualCatalogueStaticMethods {
    /**
     * Method that checks readability of file and tries to set readable.
     * @param file tracking file
     * @param warner logger
     * @return true if file is readable, or it was set; false in other cases
     */
    public static boolean checkAndFixReadability(File file, Consumer<String> warner) {
        if (!file.canRead()) {
            warner.accept(String.format("%s is not readable. Trying to set", file.getAbsolutePath()));
            if (!file.setReadable(true)) {
                warner.accept(String.format("%s cannot be set Readable", file.getAbsolutePath()));
                return false;
            }
        }
        return true;
    }

    /**
     * Throw special {@link InvalidFileException} if file dissapeared after synchronization block
     * @param id id of file
     * @param file Virtual File that can be removed
     */
    public static void throwIfCanBeRemoved(Long id, VirtualFile file) {
        if (!file.isExists()) {
            throw new InvalidFileException(
                    "File probably removed",
                    FileTroubles.FILE_PROBABLY_REMOVED,
                    id
            );
        }
    }

    /**
     * Method that checks writeability of file and tries to set writeable.
     * @param file tracking file
     * @param warner logger
     * @return true if file is writeable, or it was set; false in other cases
     */
    public static boolean checkAndFixWriteability(File file, Consumer<String> warner) {
        if (!file.canWrite()) {
            warner.accept(String.format("%s is not writable. Trying to set", file.getAbsolutePath()));
            if (!file.setWritable(true)) {
                warner.accept(String.format("%s cannot be set writable", file.getAbsolutePath()));
                return false;
            }
        }
        return true;
    }

    /**
     * Function that catch {@link InvalidFileException} and put it to TroublesHunter
     * @param func executable function that can throw {@link InvalidFileException}
     * @param hunter consumer that process exceptions
     * @return result of func
     * @param <T> any class
     */
    public static <T> Optional<T> troubleHunt(Supplier<T> func, Consumer<InvalidFileException> hunter) {
        try {
            return Optional.of(func.get());
        } catch (InvalidFileException e) {
            hunter.accept(e);
        }
        return Optional.empty();
    }

    /**
     * Function that prevent any exception
     * @param execute any function
     * @param warner Logger stream
     * @param ifThrown function executes if exception throws
     * @return result of execute
     * @param <T> any class
     */
    public static<T> T preventThrowing(Supplier<T> execute, Consumer<String> warner, Supplier<T> ifThrown) {
        try {
            return execute.get();
        } catch (Exception t) {
            warner.accept(String.format("Unknown throw: %s", t.getMessage()));
            return ifThrown.get();
        }
    }

    /**
     * directly reads from file with id
     * @param id id of file
     * @param size maximum size of result buffer
     * @param supplier produces {@link VirtualFile}
     * @param troublesHunter method that sends exception to producer
     * @param debugger Logger debug
     * @return char array of {@code size} or size of file
     */
    public static Optional<char[]> readDirectly(Long id, int size, Supplier<VirtualFile> supplier,
                                                Consumer<InvalidFileException> troublesHunter,
                                                Consumer<String> debugger) {
        debugger.accept(String.format("Reading file with id %d of size %d bytes", id, size));
        var file = supplier.get();
        Optional<char[]> ret;
        try{
            synchronized (file) {
                throwIfCanBeRemoved(id, file);
                ret = Optional.of(file.readDirectly(size));
            }
        } catch (InvalidFileException e) {
            troublesHunter.accept(e);
            return Optional.empty();
        }
        return ret;
    }

    /**
     * get path of file and protect it
     * @param id id of file
     * @param supplier function that give VirtualFile object
     * @param troublesHunter method process {@link InvalidFileException}
     * @param debugger Logger debugger
     * @return path of file or nothing if something went wrong
     */
    public static Optional<Path> getPath(Long id, Supplier<VirtualFile> supplier,
                                         Consumer<InvalidFileException> troublesHunter,
                                         Consumer<String> debugger) {
        var file = supplier.get();
        Optional<Path> ret;
        try {
            synchronized (file) {
                throwIfCanBeRemoved(id, file);
                ret = Optional.of(file.getPath());
            }
        } catch (InvalidFileException e) {
            troublesHunter.accept(e);
            return Optional.empty();
        }
        debugger.accept(String.format("Get path %s for file with id %d", ret, id));
        return ret;
    }

    /**
     * Calculate check sum of Virtual File
     * @param id id of file
     * @param supplier get Virtual File
     * @param troublesHunter process thrown {@link InvalidFileException}
     * @param debugger Logger debugger
     * @return Checksum or nothing
     */
    public static OptionalLong getCheckSum(Long id, Supplier<VirtualFile> supplier,
                                           Consumer<InvalidFileException> troublesHunter,
                                           Consumer<String> debugger) {
        var file = supplier.get();
        Long ret;
        try {
            synchronized (file) {
                throwIfCanBeRemoved(id, file);
                ret = file.calculateCheckSum();
            }
        } catch(InvalidFileException e) {
            troublesHunter.accept(e);
            return OptionalLong.empty();
        }
        debugger.accept(String.format("Checksum of file with id %d is %d", id, ret));
        return OptionalLong.of(ret);
    }

    /**
     * Directly writes to file with id
     * @param id id file
     * @param buf writing buffer
     * @param supplier function that get Virtual File
     * @param append variable that allow to add to file
     * @param troublesHunter consumer that produces thrown {@link InvalidFileException}
     * @param debugger Logger debugger
     * @return id of file or nothing
     */
    public static OptionalLong writeDirectly(Long id, char[] buf, Supplier<VirtualFile> supplier, boolean append,
                                             Consumer<InvalidFileException> troublesHunter, Consumer<String> debugger) {
        final int MAX_SIZE = 25;
        var file = supplier.get();
        debugger.accept(String.format(
                        "Trying to write %s directly to file with id %d",
                        new String(Arrays.copyOf(buf, MAX_SIZE)) + (buf.length>MAX_SIZE?"...":""),
                        id
                )
        );
        boolean success;
        try {
            synchronized (file) {
                throwIfCanBeRemoved(id, file);
                success = file.writeDirectly(buf, append);
            }
        } catch (InvalidFileException e) {
            troublesHunter.accept(e);
            return OptionalLong.empty();
        }
        debugger.accept(
                String.format(
                        "File with id %s wrote %ssuccessfully",
                        Long.toString(id, 16),
                        success?"":"un"
                )
        );
        return OptionalLong.of(id);
    }

    /**
     * Touches file by id
     * @param id id of file
     * @param creator function that get id and creates file
     * @param troublesHunter function that process thrown {@link InvalidFileException}
     * @param checkExists function that checks existing of this id
     * @param informer Logger info
     * @return id of file or nothing
     */
    public static OptionalLong touch(Long id, Supplier<Long> creator,
                                     Consumer<InvalidFileException> troublesHunter,
                                     Predicate<Long> checkExists, Consumer<String> informer) {
        if (checkExists.test(id)) {
            informer.accept(String.format("File with id %s exists already", Long.toString(id, 16)));
            return OptionalLong.of(id);
        }
        Long retId = 0L;
        try {
            retId = creator.get();
        } catch(InvalidFileException e) {
            troublesHunter.accept(e);
        }
        if (retId == null || !retId.equals(id)) {
            informer.accept(String.format("File with id %s wasn't created", Long.toString(id, 16)));
            return OptionalLong.empty();
        }
        return OptionalLong.of(id);
    }

    /**
     * Starting initialization of Virtual Catalogue by directory
     * @param directory tracking directory
     * @param warner Logger warner
     * @param constructor method that constructs Virtual File by File
     * @param troublesHunter method that produce {@link InvalidFileException}
     * @param mapPutter method that place Virtual File to inner Collection of Virtual Catalogue
     * @return true if everything is successfully; false in other cases
     */
    public static boolean startInitialization(File directory, Consumer<String> warner,
                                              Function<File, Optional<VirtualFile>> constructor,
                                              Consumer<InvalidFileException> troublesHunter,
                                              Consumer<VirtualFile> mapPutter
    ) {
        if (!directory.isDirectory()) {
            warner.accept(String.format("%s is not directory", directory));
            return false;
        }
        try {
            Stream.of(Objects.requireNonNull(directory.listFiles()))
                    .parallel()
                    .map(constructor)
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .peek(mapPutter)
                    .close();
        } catch (NullPointerException e) {
            warner.accept(
                    String.format(
                            "Something went wrong - %s is not directory and not catch",
                            directory.getAbsolutePath()
                    )
            );
        } catch (InvalidFileException e) {
            troublesHunter.accept(e);
            return false;
        }
        return true;
    }

    /**
     * Runtime checking of directory existing and possibility to read and write to it
     * @param dir tracking directory
     * @param errors Logger errors
     * @param warner Logger warner
     * @return true if everything is good; false in other cases
     */
    public static boolean checkDirectory(File dir, Consumer<String> errors, Consumer<String> warner) {
        if (!dir.exists()) {
            errors.accept(
                    String.format(
                            "Directory %s is gone. Program can work still. But The Judgment Day is coming" +
                                    "and now the program will work incorrectly. Good luck.",
                            dir
                    )
            );
            return false;
        }
        if (!checkAndFixReadability(dir, warner)) {
            errors.accept(
                    String.format(
                            "Directory %s cannot be read. Program can work still. But The Judgment Day is coming" +
                                    "and now the program will work incorrectly. Good luck.",
                            dir
                    )
            );
            return false;
        }
        if (!checkAndFixWriteability(dir, warner)) {
            errors.accept(
                    String.format(
                            "Directory %s cannot be wrote. Program can work still. But The Judgment Day is coming" +
                                    "and now the program will work incorrectly. Good luck.",
                            dir
                    )
            );
            return false;
        }
        return true;
    }

    /**
     * Deleting file
     * @param virtualFileSupplier function that get
     * @return true if file deleted successfully
     */
    public static boolean remove(Supplier<VirtualFile> virtualFileSupplier) {
        var removingFile = virtualFileSupplier.get();
        boolean ret = true;
        synchronized (removingFile) {
            if (removingFile.isExists()) {
                ret = removingFile.remove();
            }
        }
        return ret;
    }

    /**
     * Find and returns array list with diapasons of free id
     * @param minId minimal id in diapason
     * @param maxId max id in diapason
     * @param occupiedIds <b>sorted</b> set of occupied ids.
     * @return array list of free diapasons
     */
    public static ArrayList<Long[]> findFreeDiapasons(Long minId, Long maxId, Set<Long> occupiedIds) {
        var array = occupiedIds.toArray(new Long[0]);//init array from set
        ArrayList<Long[]> ret = new ArrayList<>();//returning diapason
        if (array.length == 0) {//if array is empty, return whole id diapason without processing
            ret.add(new Long[]{minId, maxId});
            return ret;
        }
        if (array[0] < minId || array[array.length - 1] > maxId) {
            //if occupied id diapason is out-bounding the id diapason
            return null;
        }
        Long startingDiapason;
        Long endingDiapason;
        if (!array[0].equals(minId)) {//if the first element of array is not equal to minId add starting diapason
            ret.add(new Long[]{minId, array[0] - 1});
        }
        if (array[0].equals(maxId)) {//if array[0] is equal to maxId returns array
            return ret;
        }
        if (array.length == 1) {//if size of array is 1, return with no processing
            ret.add(new Long[]{array[0] + 1, maxId});//edge case array[0] == maxId - 1 ret.add {maxId, maxId}
            return ret;
        }
        for (var i = 1; i < array.length; i++) {
            //array.length >= 2, so no AIoBE
            //array values diapason strongly belongs to (minId, maxId)
            if (array[i] - 1 == array[i - 1]) {//check if previous id is near to current 1<-1
                continue;
            }
            startingDiapason = array[i - 1] + 1;//Moves starting pointer to next id 1->0
            endingDiapason = array[i] - 1;//Moves ending pointer to previous id 0<-1
            ret.add(new Long[]{startingDiapason, endingDiapason});
        }
        if (!array[array.length - 1].equals(maxId)) {//Add final diapason to maxId if maxId is free
            ret.add(new Long[]{array[array.length - 1], maxId});
        }
        return ret;
    }
}
